/**
 * Copyright (C) 2014-2019 by Wen Yu.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * <p>
 * Any modifications to this file must keep this entire header intact.
 * <p>
 * Change History - most recent changes go on top of previous changes
 * <p>
 * Metadata.java
 * <p>
 * Who   Date       Description
 * ====  =========  =====================================================
 * WY    26Sep2015  Added insertComment(InputStream, OutputStream, String}
 * WY    06Jul2015  Added insertXMP(InputSream, OutputStream, XMP)
 * WY    16Apr2015  Changed insertIRB() parameter List to Collection
 * WY    16Apr2015  Removed ICC_Profile related code
 * WY    13Mar2015  initial creation
 */

package com.symaster.common.pixy.meta;

import com.symaster.common.pixy.image.ImageType;
import com.symaster.common.pixy.io.*;
import com.symaster.common.pixy.meta.adobe._8BIM;
import com.symaster.common.pixy.meta.bmp.BMPMeta;
import com.symaster.common.pixy.meta.exif.Exif;
import com.symaster.common.pixy.meta.gif.GIFMeta;
import com.symaster.common.pixy.meta.iptc.IPTCDataSet;
import com.symaster.common.pixy.meta.jpeg.JPGMeta;
import com.symaster.common.pixy.meta.png.PNGMeta;
import com.symaster.common.pixy.meta.tiff.TIFFMeta;
import com.symaster.common.pixy.meta.xmp.XMP;
import com.symaster.common.util.MetadataUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;

/**
 * Base class for image metadata.
 *
 * @author Wen Yu, yuwen_66@yahoo.com
 * @version 1.0 01/12/2015
 */
public abstract class Metadata implements MetadataReader, Iterable<MetadataEntry> {
    public static final int IMAGE_MAGIC_NUMBER_LEN = 4;
    // Obtain a logger instance
    private static final Logger LOGGER = LoggerFactory.getLogger(Metadata.class);
    protected byte[] data;
    protected boolean isDataRead;
    // Fields
    private final MetadataType type;

    public Metadata(MetadataType type) {
        this.type = type;
    }

    public Metadata(MetadataType type, byte[] data) {
        if (type == null) throw new IllegalArgumentException("Metadata type must be specified");
        if (data == null) throw new IllegalArgumentException("Input data array is null");
        if (data.length == 0) isDataRead = true; // Allow for zero length data but disable read
        this.type = type;
        this.data = data;
    }

    public static void extractThumbnails(File image, String pathToThumbnail) throws IOException {
        FileInputStream fin = new FileInputStream(image);
        extractThumbnails(fin, pathToThumbnail);
        fin.close();
    }

    public static void extractThumbnails(InputStream is, String pathToThumbnail) throws IOException {
        // ImageIO.IMAGE_MAGIC_NUMBER_LEN bytes as image magic number
        PeekHeadInputStream peekHeadInputStream = new PeekHeadInputStream(is, IMAGE_MAGIC_NUMBER_LEN);
        ImageType imageType = MetadataUtils.guessImageType(peekHeadInputStream);
        // Delegate thumbnail extracting to corresponding image tweaker.
        switch (imageType) {
            case JPG:
                JPGMeta.extractThumbnails(peekHeadInputStream, pathToThumbnail);
                break;
            case TIFF:
                RandomAccessInputStream randIS = new FileCacheRandomAccessInputStream(peekHeadInputStream);
                TIFFMeta.extractThumbnail(randIS, pathToThumbnail);
                randIS.shallowClose();
                break;
            case PNG:
                LOGGER.info("PNG image format does not contain any thumbnail");
                break;
            case GIF:
            case PCX:
            case TGA:
            case BMP:
                LOGGER.info("{} image format does not contain any thumbnails", imageType);
                break;
            default:
                peekHeadInputStream.close();
                throw new IllegalArgumentException("Thumbnail extracting is not supported for " + imageType + " image");
        }
        peekHeadInputStream.shallowClose();
    }

    public static void extractThumbnails(String image, String pathToThumbnail) throws IOException {
        extractThumbnails(new File(image), pathToThumbnail);
    }

    public static void insertComment(InputStream is, OutputStream os, String comment) throws IOException {
        insertComments(is, os, Arrays.asList(comment));
    }

    public static void insertComments(InputStream is, OutputStream os, List<String> comments) throws IOException {
        // ImageIO.IMAGE_MAGIC_NUMBER_LEN bytes as image magic number
        PeekHeadInputStream peekHeadInputStream = new PeekHeadInputStream(is, IMAGE_MAGIC_NUMBER_LEN);
        ImageType imageType = MetadataUtils.guessImageType(peekHeadInputStream);
        // Delegate IPTC inserting to corresponding image tweaker.
        switch (imageType) {
            case JPG:
                JPGMeta.insertComments(peekHeadInputStream, os, comments);
                break;
            case TIFF:
                RandomAccessInputStream randIS = new FileCacheRandomAccessInputStream(peekHeadInputStream);
                RandomAccessOutputStream randOS = new FileCacheRandomAccessOutputStream(os);
                TIFFMeta.insertComments(comments, randIS, randOS);
                randIS.shallowClose();
                randOS.shallowClose();
                break;
            case PNG:
                PNGMeta.insertComments(peekHeadInputStream, os, comments);
                break;
            case GIF:
                GIFMeta.insertComments(peekHeadInputStream, os, comments);
                break;
            case PCX:
            case TGA:
            case BMP:
                LOGGER.info("{} image format does not support comment data", imageType);
                break;
            default:
                peekHeadInputStream.close();
                throw new IllegalArgumentException("comment data inserting is not supported for " + imageType + " image");
        }
        peekHeadInputStream.shallowClose();
    }

    /**
     * @param is input image stream
     * @param os output image stream
     * @param exif Exif instance
     * @throws IOException
     */
    public static void insertExif(InputStream is, OutputStream os, Exif exif) throws IOException {
        insertExif(is, os, exif, false);
    }

    /**
     * @param is input image stream
     * @param os output image stream
     * @param exif Exif instance
     * @param update true to keep the original data, otherwise false
     * @throws IOException
     */
    public static void insertExif(InputStream is, OutputStream os, Exif exif, boolean update) throws IOException {
        // ImageIO.IMAGE_MAGIC_NUMBER_LEN bytes as image magic number
        PeekHeadInputStream peekHeadInputStream = new PeekHeadInputStream(is, IMAGE_MAGIC_NUMBER_LEN);
        ImageType imageType = MetadataUtils.guessImageType(peekHeadInputStream);
        // Delegate EXIF inserting to corresponding image tweaker.
        switch (imageType) {
            case JPG:
                JPGMeta.insertExif(peekHeadInputStream, os, exif, update);
                break;
            case TIFF:
                RandomAccessInputStream randIS = new FileCacheRandomAccessInputStream(peekHeadInputStream);
                RandomAccessOutputStream randOS = new FileCacheRandomAccessOutputStream(os);
                TIFFMeta.insertExif(randIS, randOS, exif, update);
                randIS.shallowClose();
                randOS.shallowClose();
                break;
            case GIF:
            case PCX:
            case TGA:
            case BMP:
            case PNG:
                LOGGER.info("{} image format does not support EXIF data", imageType);
                break;
            default:
                peekHeadInputStream.close();
                throw new IllegalArgumentException("EXIF data inserting is not supported for " + imageType + " image");
        }
        peekHeadInputStream.shallowClose();
    }

    public static void insertICCProfile(InputStream is, OutputStream out, byte[] icc_profile) throws IOException {
        // ImageIO.IMAGE_MAGIC_NUMBER_LEN bytes as image magic number
        PeekHeadInputStream peekHeadInputStream = new PeekHeadInputStream(is, IMAGE_MAGIC_NUMBER_LEN);
        ImageType imageType = MetadataUtils.guessImageType(peekHeadInputStream);
        // Delegate ICCP inserting to corresponding image tweaker.
        switch (imageType) {
            case JPG:
                JPGMeta.insertICCProfile(peekHeadInputStream, out, icc_profile);
                break;
            case TIFF:
                RandomAccessInputStream randIS = new FileCacheRandomAccessInputStream(peekHeadInputStream);
                RandomAccessOutputStream randOS = new FileCacheRandomAccessOutputStream(out);
                TIFFMeta.insertICCProfile(icc_profile, 0, randIS, randOS);
                randIS.shallowClose();
                randOS.shallowClose();
                break;
            case GIF:
            case PCX:
            case TGA:
            case BMP:
                LOGGER.info("{} image format does not support ICCProfile data", imageType);
                break;
            default:
                peekHeadInputStream.close();
                throw new IllegalArgumentException("ICCProfile data inserting is not supported for " + imageType + " image");
        }
        peekHeadInputStream.shallowClose();
    }

    public static void insertIPTC(InputStream is, OutputStream out, Collection<IPTCDataSet> iptcs) throws IOException {
        insertIPTC(is, out, iptcs, false);
    }

    public static void insertIPTC(InputStream is, OutputStream out, Collection<IPTCDataSet> iptcs, boolean update) throws IOException {
        // ImageIO.IMAGE_MAGIC_NUMBER_LEN bytes as image magic number
        PeekHeadInputStream peekHeadInputStream = new PeekHeadInputStream(is, IMAGE_MAGIC_NUMBER_LEN);
        ImageType imageType = MetadataUtils.guessImageType(peekHeadInputStream);
        // Delegate IPTC inserting to corresponding image tweaker.
        switch (imageType) {
            case JPG:
                JPGMeta.insertIPTC(peekHeadInputStream, out, iptcs, update);
                break;
            case TIFF:
                RandomAccessInputStream randIS = new FileCacheRandomAccessInputStream(peekHeadInputStream);
                RandomAccessOutputStream randOS = new FileCacheRandomAccessOutputStream(out);
                TIFFMeta.insertIPTC(randIS, randOS, iptcs, update);
                randIS.shallowClose();
                randOS.shallowClose();
                break;
            case PNG:
            case GIF:
            case PCX:
            case TGA:
            case BMP:
                LOGGER.info("{} image format does not support IPTC data", imageType);
                break;
            default:
                peekHeadInputStream.close();
                throw new IllegalArgumentException("IPTC data inserting is not supported for " + imageType + " image");
        }
        peekHeadInputStream.shallowClose();
    }

    public static void insertIRB(InputStream is, OutputStream out, Collection<_8BIM> bims) throws IOException {
        insertIRB(is, out, bims, false);
    }

    public static void insertIRB(InputStream is, OutputStream out, Collection<_8BIM> bims, boolean update) throws IOException {
        // ImageIO.IMAGE_MAGIC_NUMBER_LEN bytes as image magic number
        PeekHeadInputStream peekHeadInputStream = new PeekHeadInputStream(is, IMAGE_MAGIC_NUMBER_LEN);
        ImageType imageType = MetadataUtils.guessImageType(peekHeadInputStream);
        // Delegate IRB inserting to corresponding image tweaker.
        switch (imageType) {
            case JPG:
                JPGMeta.insertIRB(peekHeadInputStream, out, bims, update);
                break;
            case TIFF:
                RandomAccessInputStream randIS = new FileCacheRandomAccessInputStream(peekHeadInputStream);
                RandomAccessOutputStream randOS = new FileCacheRandomAccessOutputStream(out);
                TIFFMeta.insertIRB(randIS, randOS, bims, update);
                randIS.shallowClose();
                randOS.shallowClose();
                break;
            case PNG:
            case GIF:
            case PCX:
            case TGA:
            case BMP:
                LOGGER.info("{} image format does not support IRB data", imageType);
                break;
            default:
                peekHeadInputStream.close();
                throw new IllegalArgumentException("IRB data inserting is not supported for " + imageType + " image");
        }
        peekHeadInputStream.shallowClose();
    }

    public static void insertIRBThumbnail(InputStream is, OutputStream out, BufferedImage thumbnail) throws IOException {
        // ImageIO.IMAGE_MAGIC_NUMBER_LEN bytes as image magic number
        PeekHeadInputStream peekHeadInputStream = new PeekHeadInputStream(is, IMAGE_MAGIC_NUMBER_LEN);
        ImageType imageType = MetadataUtils.guessImageType(peekHeadInputStream);
        // Delegate IRB thumbnail inserting to corresponding image tweaker.
        switch (imageType) {
            case JPG:
                JPGMeta.insertIRBThumbnail(peekHeadInputStream, out, thumbnail);
                break;
            case TIFF:
                RandomAccessInputStream randIS = new FileCacheRandomAccessInputStream(peekHeadInputStream);
                RandomAccessOutputStream randOS = new FileCacheRandomAccessOutputStream(out);
                TIFFMeta.insertThumbnail(randIS, randOS, thumbnail);
                randIS.shallowClose();
                randOS.shallowClose();
                break;
            case PNG:
            case GIF:
            case PCX:
            case TGA:
            case BMP:
                LOGGER.info("{} image format does not support IRB thumbnail", imageType);
                break;
            default:
                peekHeadInputStream.close();
                throw new IllegalArgumentException("IRB thumbnail inserting is not supported for " + imageType + " image");
        }
        peekHeadInputStream.shallowClose();
    }

    public static void insertXMP(InputStream is, OutputStream out, XMP xmp) throws IOException {
        // ImageIO.IMAGE_MAGIC_NUMBER_LEN bytes as image magic number
        PeekHeadInputStream peekHeadInputStream = new PeekHeadInputStream(is, IMAGE_MAGIC_NUMBER_LEN);
        ImageType imageType = MetadataUtils.guessImageType(peekHeadInputStream);
        // Delegate XMP inserting to corresponding image tweaker.
        switch (imageType) {
            case JPG:
                JPGMeta.insertXMP(peekHeadInputStream, out, xmp); // No ExtendedXMP
                break;
            case TIFF:
                RandomAccessInputStream randIS = new FileCacheRandomAccessInputStream(peekHeadInputStream);
                RandomAccessOutputStream randOS = new FileCacheRandomAccessOutputStream(out);
                TIFFMeta.insertXMP(xmp, randIS, randOS);
                randIS.shallowClose();
                randOS.shallowClose();
                break;
            case PNG:
                PNGMeta.insertXMP(peekHeadInputStream, out, xmp);
                break;
            case GIF:
                GIFMeta.insertXMPApplicationBlock(peekHeadInputStream, out, xmp);
                break;
            case PCX:
            case TGA:
            case BMP:
                LOGGER.info("{} image format does not support XMP data", imageType);
                break;
            default:
                peekHeadInputStream.close();
                throw new IllegalArgumentException("XMP inserting is not supported for " + imageType + " image");
        }
        peekHeadInputStream.shallowClose();
    }

    public static void insertXMP(InputStream is, OutputStream out, String xmp) throws IOException {
        // ImageIO.IMAGE_MAGIC_NUMBER_LEN bytes as image magic number
        PeekHeadInputStream peekHeadInputStream = new PeekHeadInputStream(is, IMAGE_MAGIC_NUMBER_LEN);
        ImageType imageType = MetadataUtils.guessImageType(peekHeadInputStream);
        // Delegate XMP inserting to corresponding image tweaker.
        switch (imageType) {
            case JPG:
                JPGMeta.insertXMP(peekHeadInputStream, out, xmp, null); // No ExtendedXMP
                break;
            case TIFF:
                RandomAccessInputStream randIS = new FileCacheRandomAccessInputStream(peekHeadInputStream);
                RandomAccessOutputStream randOS = new FileCacheRandomAccessOutputStream(out);
                TIFFMeta.insertXMP(xmp, randIS, randOS);
                randIS.shallowClose();
                randOS.shallowClose();
                break;
            case PNG:
                PNGMeta.insertXMP(peekHeadInputStream, out, xmp);
                break;
            case GIF:
                GIFMeta.insertXMPApplicationBlock(peekHeadInputStream, out, xmp);
                break;
            case PCX:
            case TGA:
            case BMP:
                LOGGER.info("{} image format does not support XMP data", imageType);
                break;
            default:
                peekHeadInputStream.close();
                throw new IllegalArgumentException("XMP inserting is not supported for " + imageType + " image");
        }
        peekHeadInputStream.shallowClose();
    }

    public static Map<MetadataType, Metadata> readMetadata(File image) throws IOException {
        FileInputStream fin = new FileInputStream(image);
        Map<MetadataType, Metadata> metadataMap = readMetadata(fin);
        fin.close();

        return metadataMap;
    }

    /**
     * Reads all metadata associated with the input image
     *
     * @param is InputStream for the image
     * @return a list of Metadata for the input stream
     * @throws IOException
     */
    public static Map<MetadataType, Metadata> readMetadata(InputStream is) throws IOException {
        // Metadata map for all the Metadata read
        Map<MetadataType, Metadata> metadataMap = new HashMap<MetadataType, Metadata>();
        // ImageIO.IMAGE_MAGIC_NUMBER_LEN bytes as image magic number
        PeekHeadInputStream peekHeadInputStream = new PeekHeadInputStream(is, IMAGE_MAGIC_NUMBER_LEN);
        ImageType imageType = MetadataUtils.guessImageType(peekHeadInputStream);
        // Delegate metadata reading to corresponding image tweakers.
        switch (imageType) {
            case JPG:
                metadataMap = JPGMeta.readMetadata(peekHeadInputStream);
                break;
            case TIFF:
                RandomAccessInputStream randIS = new FileCacheRandomAccessInputStream(peekHeadInputStream);
                metadataMap = TIFFMeta.readMetadata(randIS);
                randIS.shallowClose();
                break;
            case PNG:
                metadataMap = PNGMeta.readMetadata(peekHeadInputStream);
                break;
            case GIF:
                metadataMap = GIFMeta.readMetadata(peekHeadInputStream);
                break;
            case BMP:
                metadataMap = BMPMeta.readMetadata(peekHeadInputStream);
                break;
            default:
                peekHeadInputStream.close();
                throw new IllegalArgumentException("Metadata reading is not supported for " + imageType + " image");

        }
        peekHeadInputStream.shallowClose();

        return metadataMap;
    }

    public static Map<MetadataType, Metadata> readMetadata(String image) throws IOException {
        return readMetadata(new File(image));
    }

    /**
     * Remove meta data from image
     *
     * @param is InputStream for the input image
     * @param os OutputStream for the output image
     * @throws IOException
     */
    public static Map<MetadataType, Metadata> removeMetadata(InputStream is, OutputStream os, MetadataType... metadataTypes) throws IOException {
        // ImageIO.IMAGE_MAGIC_NUMBER_LEN bytes as image magic number
        PeekHeadInputStream peakHeadInputStream = new PeekHeadInputStream(is, IMAGE_MAGIC_NUMBER_LEN);
        ImageType imageType = MetadataUtils.guessImageType(peakHeadInputStream);
        // The map of removed metadata
        Map<MetadataType, Metadata> metadataMap = Collections.emptyMap();
        // Delegate meta data removing to corresponding image tweaker.
        switch (imageType) {
            case JPG:
                metadataMap = JPGMeta.removeMetadata(peakHeadInputStream, os, metadataTypes);
                break;
            case TIFF:
                RandomAccessInputStream randIS = new FileCacheRandomAccessInputStream(peakHeadInputStream);
                RandomAccessOutputStream randOS = new FileCacheRandomAccessOutputStream(os);
                metadataMap = TIFFMeta.removeMetadata(randIS, randOS, metadataTypes);
                randIS.shallowClose();
                randOS.shallowClose();
                break;
            case PCX:
            case TGA:
            case BMP:
                LOGGER.info("{} image format does not support meta data", imageType);
                break;
            default:
                peakHeadInputStream.close();
                throw new IllegalArgumentException("Metadata removing is not supported for " + imageType + " image");
        }

        peakHeadInputStream.shallowClose();

        return metadataMap;
    }

    public void ensureDataRead() {
        if (!isDataRead) {
            try {
                read();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public byte[] getData() {
        if (data != null)
            return data.clone();

        return null;
    }

    public MetadataType getType() {
        return type;
    }

    public boolean isDataRead() {
        return isDataRead;
    }

    /**
     * Writes the metadata out to the output stream
     *
     * @param out OutputStream to write the metadata to
     * @throws IOException
     */
    public void write(OutputStream out) throws IOException {
        byte[] data = getData();
        if (data != null)
            out.write(data);
    }
}
